package org.sanguine.plugin.timetracker.providers;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.sanguine.plugin.timetracker.pojo.*;
import org.sanguine.plugin.timetracker.pojo.Timer;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
 * <p>
 * Tracker Service Provider for HARVEST
 * </p>
 * <p>
 * </p>
 * <b>Date:</b> 13.10.2014
 *
 * @author Alexey_Kuznetsov
 */
public class HarvestServiceProvider implements TrackerServiceProvider {
    private final static String USER_AGENT = "Intellij IDEA plugin (kuzn2k@gmail.com)";
    private final static String WHO_AM_I = "/account/who_am_i";
    private final static String GET_ALL_PROJECTS = "/projects";
    private final static String GET_ALL_TASKS = "/tasks";
    private final static String GET_TASKS_ASSIGNMENT = "/projects/%d/task_assignments";
    private final static String GET_KNOWN_TIMERS = "/daily";
    private final static String ADD_DAY_ENTRY = "/daily/add";
    private final static String GET_TIMER = "/daily/show/%d";
    private final static String TOGGLE_TIMER = "/daily/timer/%d";

    private URL endPoint;
    private HttpClientContext approvedContext;

    private class TaskAssignment {
        Integer id;
        Integer taskId;
        Boolean deactivated;
    }

    private class TaskAssignmentElement {
        TaskAssignment taskAssignment;
    }

    private class ProjectElement {
        Project project;
    }

    private class TaskElement {
        Task task;
    }

    @Override
    public boolean initialize(String endpoint, String userName, char[] password) {
        approvedContext = null;

        if (endpoint == null || endpoint.isEmpty())
            throw new IllegalArgumentException("EndPoint parameter must be set");
        if (userName == null || userName.isEmpty())
            throw new IllegalArgumentException("UserName parameter must be set");
        if (password == null || password.length == 0)
            throw new IllegalArgumentException("Password parameter must be set");

        try {
            this.endPoint = new URL(endpoint);
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("EndPoint is malformed");
        }

        Credentials newCredentials = new UsernamePasswordCredentials(userName, new String(password));

        HttpHost targetHost = new HttpHost(endPoint.getHost(), endPoint.getPort(), endPoint.getProtocol());
        CredentialsProvider credsProvider = new BasicCredentialsProvider();
        credsProvider.setCredentials(AuthScope.ANY, newCredentials);

        AuthCache authCache = new BasicAuthCache();
        authCache.put(targetHost, new BasicScheme());

        final HttpClientContext context = HttpClientContext.create();
        context.setCredentialsProvider(credsProvider);
        context.setAuthCache(authCache);

        if (getObjectFromRestService(context, WHO_AM_I, AccountInfo.class) != null) {
            approvedContext = context;
        }

        return approvedContext != null;
    }

    @Override
    public AccountInfo getAccountInfo() {
        if (approvedContext == null) {
            throw new IllegalStateException("Service provider is not properly initialized");
        }

        return getObjectFromRestService(approvedContext, WHO_AM_I, AccountInfo.class);
    }

    @Override
    public String getDescription() {
        return "Provider for HARVEST Time tracking service (via RESTful)";
    }

    @Override
    public float startTimer(int timerId) {
        return setTimerState(timerId, true);
    }

    @Override
    public float stopTimer(int timerId) {
        return setTimerState(timerId, false);
    }

    private float setTimerState(int timerId, boolean requiredActiveState) {
        if (isStarted(timerId) != requiredActiveState) {
            Timer timer = getObjectFromRestService(approvedContext, String.format(TOGGLE_TIMER, timerId), Timer.class);
            return timer.hours != null ? timer.hours : 0.0f;
        } else {
            return getTimerValue(timerId);
        }
    }

    private boolean isStarted(int timerId) {
        Timer foundTimer = getTimer(timerId);
        return foundTimer.timerStartedAt != null;
    }

    @Override
    public float getTimerValue(int timerId) {
        Timer timer = getTimer(timerId);
        return timer.hours != null ? timer.hours : 0.0f;
    }

    private Timer getTimer(int timerId) {
        if (approvedContext == null) {
            throw new IllegalStateException("Service provider is not properly initialized");
        }

        return getObjectFromRestService(approvedContext, String.format(GET_TIMER, timerId), Timer.class);
    }

    @Override
    public List<Project> getProjects() {
        if (approvedContext == null) {
            throw new IllegalStateException("Service provider is not properly initialized");
        }

        String response = doHttpRequest(approvedContext, new HttpGet(GET_ALL_PROJECTS));
        ProjectElement[] array = createCustomGson().fromJson(response, ProjectElement[].class);

        Map<Integer, Task> tasks = getAllTasksMap();

        List<Project> projects = new ArrayList<>();
        for (ProjectElement element : array) {
            if (element.project.active && element.project.id != null) {
                element.project.tasks = getTaskAssignments(element.project.id, tasks);
                projects.add(element.project);
            }
        }

        return projects;
    }

    private Map<Integer, Task> getAllTasksMap() {
        String response = doHttpRequest(approvedContext, new HttpGet(GET_ALL_TASKS));
        TaskElement[] array = createCustomGson().fromJson(response, TaskElement[].class);

        Map<Integer, Task> tasks = new HashMap<>();
        for (TaskElement element : array) {
            if (element.task.id != null) {
                tasks.put(element.task.id, element.task);
            }
        }
        return tasks;
    }

    private List<Task> getTaskAssignments(Integer projectId, Map<Integer, Task> allTasks) {
        String response = doHttpRequest(approvedContext, new HttpGet(String.format(GET_TASKS_ASSIGNMENT, projectId)));
        TaskAssignmentElement[] array = createCustomGson().fromJson(response, TaskAssignmentElement[].class);

        List<Task> tasks = new ArrayList<>();
        for (TaskAssignmentElement element : array) {
            if (element.taskAssignment.deactivated != null && !element.taskAssignment.deactivated
                    && element.taskAssignment.taskId != null && allTasks.containsKey(element.taskAssignment.taskId)) {
                tasks.add(allTasks.get(element.taskAssignment.taskId));
            }
        }

        return tasks;
    }

    @Override
    public List<Task> getTasks() {
        if (approvedContext == null) {
            throw new IllegalStateException("Service provider is not properly initialized");
        }

        Map<Integer, Task> tasks = getAllTasksMap();
        return new ArrayList<>(tasks.values());
    }

    @Override
    public Timer getActiveTimer() {
        DailyList knownTimers = getTimerList();
        return findActiveTimer(knownTimers);
    }

    private Timer findActiveTimer(DailyList knownTimers) {
        for (Timer timer : knownTimers.dayEntries) {
            if (timer.timerStartedAt != null) {
                return timer;
            }
        }
        return null;
    }

    @Override
    public Timer createTimer(int projectId, int taskId, String notes) {
        if (approvedContext == null) {
            throw new IllegalStateException("Service provider is not properly initialized");
        }

        DailyList knownTimers = getTimerList();

        Timer timer = findTimer(knownTimers, projectId, taskId, notes);
        Timer activeTimer = findActiveTimer(knownTimers);

        if (timer == null) {
            if (activeTimer != null) {
                stopTimer(activeTimer.id);
            }
            timer = createNewDayEntry(projectId, taskId, notes);
        } else if (timer.timerStartedAt == null) {
            if (activeTimer != null) {
                stopTimer(activeTimer.id);
            }
            startTimer(timer.id);
        }
        return timer;
    }

    private Timer createNewDayEntry(int projectId, int taskId, String notes) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy", Locale.US);
        String requestBody = String.format(
                "{\"notes\":\"%s\",\"project_id\":\"%d\",\"task_id\":\"%d\",\"spent_at\":\"%s\"}",
                notes == null ? "" : notes, projectId, taskId, dateFormat.format(new Date()));

        Timer timer = postObjectToRestService(approvedContext, ADD_DAY_ENTRY, Timer.class, requestBody,
                "application/json");
        if (timer != null && timer.id == null) {
            throw new RuntimeException("Service return new day entry with null id");
        }
        return timer;
    }

    private Timer findTimer(DailyList knownTimers, int projectId, int taskId, String notes) {
        if (knownTimers != null && knownTimers.dayEntries != null) {
            for (Timer timer : knownTimers.dayEntries) {
                if (timer.projectId == projectId
                        && timer.taskId == taskId
                        && (notes == null || notes.isEmpty() || notes.equalsIgnoreCase(timer.notes))) {
                    return timer;
                }
            }
        }
        return null;
    }

    private DailyList getTimerList() {
        return getObjectFromRestService(approvedContext, GET_KNOWN_TIMERS, DailyList.class);
    }

    private <T> T getObjectFromRestService(HttpClientContext context, String path, Class<T> objectClass) {
        HttpRequestBase request = new HttpGet(path);
        String result = doHttpRequest(context, request);
        return createCustomGson().fromJson(result, objectClass);
    }

    private <T> T postObjectToRestService(HttpClientContext context, String path, Class<T> objectClass, String body,
            String bodyType) {
        HttpPost request = new HttpPost(path);
        StringEntity input;
        try {
            input = new StringEntity(body);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        input.setContentType(bodyType);
        request.setEntity(input);

        return createCustomGson().fromJson(doHttpRequest(context, request), objectClass);
    }

    private Gson createCustomGson() {
        return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
    }

    private String doHttpRequest(HttpClientContext context, HttpRequestBase request) {
        try (CloseableHttpClient httpClient = HttpClientBuilder.create()
                .setUserAgent(USER_AGENT)
                .build()) {

            request.addHeader("accept", "application/json");

            HttpHost host = new HttpHost(endPoint.getHost(), endPoint.getPort(), endPoint.getProtocol());

            HttpResponse response = httpClient.execute(host, request, context);

            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK
                    || response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) {
                HttpEntity httpEntity = response.getEntity();
                String result = EntityUtils.toString(httpEntity);
                return result;
            } else {
                return "";
            }
        } catch (ClientProtocolException e) {
            throw new RuntimeException("Client protocol exception with host " + endPoint.toString(), e);
        } catch (IOException e) {
            throw new RuntimeException("IO error with host " + endPoint.toString(), e);
        }
    }

    public static TrackerServiceProvider getInstance() {
        return new HarvestServiceProvider();
    }

    // Disable instantiating via new
    private HarvestServiceProvider() {

    }
}
